package com.m3958.logcollector.repository;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.m3958.logcollector.datacommons.util.AllParams;
import com.m3958.logcollector.datacommons.util.KeyAndCount;
import com.m3958.logcollector.domain.Person;
import com.m3958.logcollector.domain.Role;
import com.m3958.logcollector.util.common.CommonJsonFieldExtractor;
import com.m3958.logcollector.util.common.M3958SecurityUtil;
import com.m3958.logcollector.vo.RoleNames;


public class PersonRepositoryImpl extends SimpleJpaRepository<Person, Long>  implements PersonRepositoryCustom, ApplicationContextAware {

    private final EntityManager entityManager;

    private ApplicationContext context;

    private final JpaEntityInformation<Person, ?> entityInformation;
    
    private static Splitter commaSplitter = Splitter.on(',').omitEmptyStrings().trimResults();
    
    @Autowired
    private CommonJsonFieldExtractor commonJsonFieldExtractor;
    
    
    @Autowired
    public PersonRepositoryImpl(EntityManager entityManager) {
        super(Person.class, entityManager);
        this.entityManager = entityManager;
        this.entityInformation = JpaEntityInformationSupport.getEntityInformation(Person.class, entityManager);
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }
    
    /* (non-Javadoc)
     * @see org.springframework.data.jpa.repository.support.SimpleJpaRepository#save(java.lang.Object)
     */
    @Override
    public <S extends Person> S save(S entity) {
        RoleRepository roleRepo = context.getBean(RoleRepository.class);
        if (entityInformation.isNew(entity)) {
            Set<Role> roles = entity.getRoles().stream().filter(r -> RoleNames.USER.equals(r.getName())).collect(Collectors.toSet());
            if (roles.isEmpty()) {
                roles.add(roleRepo.findByName(RoleNames.USER));
            }
        } else {
            entity = updateExist(entity);
        }
        return super.save(entity);
    }
    
    
    @PreAuthorize("hasAnyRole('PERSON_MANAGER', 'PERSON_CREATOR') or principal.id == #entity.id")
    private <S extends Person> S updateExist(S entity) {
        return entity;
    }
    
    @Override
    public Page<Person> findAll(Pageable pageable) {
        return findQ(new AllParams(), pageable);
    }
    
    /* (non-Javadoc)
     * @see org.springframework.data.jpa.repository.support.SimpleJpaRepository#findOne(java.io.Serializable)
     */
    @Override
    public Person findOne(Long id) {
        if (M3958SecurityUtil.getLoginPersonId() == id || M3958SecurityUtil.hasRole(RoleNames.PERSON_MANAGER)) {
            return super.findOne(id);
        }
        throw new AccessDeniedException("can only fetch yours.");
    }
    
    /* (non-Javadoc)
     * @see com.m3958.datacommons.repository.M3958DataRepository#findQ(com.m3958.datacommons.utils.AllParams, org.springframework.data.domain.Pageable)
     */
    @Override
    public Page<Person> findQ(AllParams q, Pageable pageable) {
        return super.findAll(pageable);
    }
    
//    private Page<Person> findQInternal(long l, AllParams q, Pageable pageable) {
//        if (l == -1) {
//            return super.findAll(pageable);
//        } else {
//            TypedQuery<Person> tq = getQueryQ();
//            tq.setFirstResult(pageable.getOffset());
//            tq.setMaxResults(pageable.getPageSize());
//            List<Person> apps = tq.getResultList();
//            Long total = getCountQueryQ().getSingleResult();
//            return new PageImpl<>(apps, pageable, total);
//        }
//    }


//    private TypedQuery<Person> getQueryQ() {
//        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//        CriteriaQuery<Person> cq = cb.createQuery(App.class);
//        Root<AppLink> alRoot = cq.from(AppLink.class);
//        Path<App> app = alRoot.get(AppLink_.app); 
//        cq.select(app);
//        cq.where(cb.equal(alRoot.get(AppLink_.creator).get(ScanhubUser_.id), ScanhubSecurityUtil.getLoginUserId()));
//        return entityManager.createQuery(cq);
//    }
//    
//    private TypedQuery<Long> getCountQueryQ() {
//        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//        CriteriaQuery<Long> cq = cb.createQuery(Long.class);
//        Root<AppLink> alRoot = cq.from(AppLink.class);
//        Path<App> app = alRoot.get(AppLink_.app); 
//        cq.select(cb.countDistinct(app));
//        cq.where(cb.equal(alRoot.get(AppLink_.creator).get(ScanhubUser_.id), ScanhubSecurityUtil.getLoginUserId()));
//        return entityManager.createQuery(cq);
//    }

    /* (non-Javadoc)
     * @see com.m3958.datacommons.repository.M3958DataRepository#keyAndCount(com.m3958.datacommons.utils.AllParams)
     */
    @Override
    public List<KeyAndCount> keyAndCount(AllParams arg0) {
        // TODO Auto-generated method stub
        return null;
    }

    /* (non-Javadoc)
     * @see com.m3958.datacommons.repository.M3958DataRepository#updateByPayload(com.fasterxml.jackson.databind.JsonNode, java.lang.Long)
     */
    @Override
    public Person updateByPayload(JsonNode payload, Long id) throws JsonProcessingException {

        if (payload.path("id").isMissingNode()) {
            payload = payload.path("person");
        }
        
        final JsonNode fpayload = payload;
        
        List<String> patches = Lists.newArrayList();
        if (payload.has("_patch")) {
            ArrayNode an = (ArrayNode) payload.get("_patch");
            for (JsonNode jn : an) {
                patches.add(jn.asText());
            }
        }
        
        RoleRepository roleRepo = context.getBean(RoleRepository.class);
        Person user = findOne(id);
        
        if (patches.contains("level")) {
            int level = payload.has("level") ? payload.get("level").asInt(1) : 1;
            user.setLevel(level);
        }

        if (patches.contains("roles")) {
            changeRoles(payload, roleRepo, user);
        }
        
        Set<String> patchFields = commonJsonFieldExtractor.patchFields(payload);
        if (!patchFields.isEmpty()) {
            patchFields.stream().forEach(fn -> updateOneField(user, fpayload, fn));
        }
        return save(user);
    }
    
    private void updateOneField(Person person, JsonNode jn, String fieldName) {
        switch (fieldName) {
        case "name":
            String name = jn.get(fieldName).asText();
            person.setName(name);
            break;
        default:
            break;
        }
    }
    
    protected void changeRoles(JsonNode payload, RoleRepository roleRepo, Person person) {
        Set<Role> updatedRoles = Sets.newHashSet();

        JsonNode roleNamesNode = payload.path("roleNames");

        if (roleNamesNode.isMissingNode()) {
            JsonNode rolesNode = payload.path("roles");
            if (!rolesNode.isMissingNode()) {
                List<Long> rids = Lists.newArrayList();

                for (JsonNode rnd : rolesNode) {
                    if (rnd.isObject()) {
                        rids.add(rnd.get("id").asLong());
                    } else {
                        rids.add(rnd.asLong());
                    }
                }
                updatedRoles = rids.stream()//
                        .map(lid -> entityManager.find(Role.class, lid))//
                        .collect(Collectors.toSet());
            }
        } else {
            updatedRoles = commaSplitter.splitToList(roleNamesNode.asText())//
                    .stream().map(sid -> Long.valueOf(sid))//
                    .map(lid -> roleRepo.findOne(lid))//
                    .collect(Collectors.toSet());

        }

        boolean hasUserRole = updatedRoles.stream().anyMatch(r -> RoleNames.USER.equals(r.getName()));

        if (!hasUserRole) {
            updatedRoles.add(roleRepo.findByName(RoleNames.USER));
        }

        person.setRoles(updatedRoles);
    }
}
